<?php

class TH_DbItem extends TH_EventModel {
    const S_INVALID = 'S_INVALID';
    const S_OK      = 'S_OK';
    const S_NEW     = 'S_NEW';
    const S_CHANGED = 'S_CHANGED';
    const S_DELETE  = 'S_DELETE';

    /**
     * @var TH_DbBase
     */
    private $tdb;
    private $name;
    private $cls;
    private $state;
//    private $state_object;

//    private $_props = array();

    protected $behavior = array();    // NOTE: it removes in init_rels
    protected $props    = array();    // NOTE: it removes in init_rels
    protected $rels     = array();    // NOTE: it removes in init_rels


    function __construct(TH_DbBase $tdb,
                         $name,
                         Array $values,//) {
                         $state = self::S_NEW) {    // internal use

        $this->tdb   = $tdb;
        $this->name  = $name;
        $this->cls   = get_class($this);

        $this->state = $state;

        $this->init_behavior()
             ->init_props($values)
             ->init_hooks();

        if (self::S_NEW && $this->state) {
            $this->emmit("on_create___object");
        } elseif (self::S_OK) {
            $this->emmit("on_load___object");
        } else {
            xdbg()->err($this->state);
            throw new Exception("Invalid state on {$this->name} ({$this->cls})");
        }
    }

    /**
	 * @return TH_DbItem
	 */
    private function init_behavior() {
        if (!array_key_exists('b.load', $this->behavior)) {
            $this->behavior['b.load'] = TH_DbBase::L_REQ;
        }

        return $this;
    }

    /**
	 * @return TH_DbItem
	 */
	private function init_props(Array $values) {

	    $this->props = $this->props;
//        unset ($this->props);    // props should not be available for user

        $this->isPropExists('_object', TRUE);

	    $this->props['_object'] = array(
	        'v.type'   => TH_DbBase::T_VIRTUAL,
	        'v.access' => TH_DbBase::A_DENIED,
	        'v.default' => &$this,
	    );

	    foreach ($this->props as $name => $args) {
	        // v.* - value description keys
	        if (!array_key_exists('v.type', $args)) {
                $args['v.type'] = TH_DbBase::T_STRING;
            }

	        if (array_key_exists('v.default', $args)) {
	            // do we realy need this check? maybe it is good practice to write right code?
	            if (NULL !== $args['v.default'] // NOTE: some props could not be nullable!!!
	                && !$this->isType($args['v.default'], $args['v.type'])) {
	                throw new Exception("Invalid default value type for $name prop on {$this->name} ({$this->cls})");
	            }
            } else {
                // NOTE: in this case T_VIRTUAL AND T_SERIALIZABLE becomes NULL !!!
                $args['v.default'] = NULL;

            }

            if (!array_key_exists('v.access', $args)) {
                $args['v.access'] = TH_DbBase::A_FULL;
            }

//            xdbg()->info("\$this->isPropExists($name, TRUE);" );
//			xdbg()->msg($this->props);
			// FIXME: what is it???? o_O i don't remember what this does
//            $this->isPropExists($name, TRUE);
            $this->props[$name] = array('v.type'   => $args['v.type'],
            					         'v.access' => $args['v.access'],
                                         'r.name'   => array());

            // search for possible binded relations and initialize them
            foreach ($this->rels as $r_name => $r_args) {
                if (!array_key_exists('r.key', $r_args)) {
                    throw new Exception("No key set in {$r_name} relation on {$this->name} ({$this->cls})");
                } else {
                    if ($r_args['r.key'] == $name) {
                        if (!array_key_exists('r.children', $this->props[$name])) {
                            $this->props[$name]['r.children'] = array();
                        }
                        // say about one more relation to it's parent
                        array_push($this->props[$name]['r.children'], $r_name);
                        // init relation
                        $this->init_rel($r_name);
                    }
                }

            }

            // s.* - system values
            if (array_key_exists('r.children', $this->props[$name])) {
                // houston, we've got a problem! key property detected!
                // 1. Force READONLY
                $this->props[$name]['v.access'] = TH_DbBase::A_READONLY;
                $this->props[$name]['s.key'] = TRUE; // indicates that prop is key
            } else {
                $this->props[$name]['s.key'] = FALSE;
            }

            $this->props[$name]['s.value'] = array_key_exists($name, $values) && self::S_NEW != $this->state
                                           ? $this->setType($values[$name], $args['v.type'])
                                           : $args['v.default'];
        }

        unset($this->rels);    // remove props container at all, we'll not use it
	    return $this;
	}

	private function init_rel ($name) {
        $args = $this->rels[$name];

        // r.* - relation description keys
        if (!array_key_exists('r.access', $args)) {
            $args['r.access'] = TH_DbBase::A_FULL;
        }

        if (!array_key_exists('r.type', $args)) {
            $args['r.type'] = TH_DbBase::R_AUTO;
        }

        if (!array_key_exists('r.path', $args)) {
            throw new Exception("Relation path (r.path) is not set for {$name} relation on {$this->name} ({$this->cls})");
        }

        $args['r.path'] = explode(':', $args['r.path']);    // broker:table:key OR table:key

        $rel_sz = sizeof($args['r.path']);

        if ($rel_sz == 2) {    // without broker
            $obj_name = $args['r.path'][0];
            $binding = array(
                array(TH_DbBase::D_STORAGE, $this->name,        $args['r.key']),
                array(TH_DbBase::D_STORAGE, $args['r.path'][0], $args['r.path'][1])
            );

//            xdbg()->err($args['r.type'], TH_DbBase::R_DIRECT != $args['r.type'], TH_DbBase::R_BACK != $args['r.type'], $args);
            if (TH_DbBase::R_AUTO == $args['r.type']) {
                if ($binding[1][2] == $binding[0][1] . '_' . $binding[0][2]) {
                    // _, table, id
                    // _, remote, table_id
                    $args['r.type'] = TH_DbBase::R_DIRECT;
                } elseif ($binding[0][2] == $binding[1][1] . '_' . $binding[1][2]) {
                    // _, remote, table_id
                    // _, table, id
                    $args['r.type'] = TH_DbBase::R_BACK;
                } else {
                    throw new Exception("Unable to autodetect relation type (r.type) for {$name} relation on {$this->name} ({$this->cls})");
                }

            } elseif (TH_DbBase::R_DIRECT != $args['r.type']
                      && TH_DbBase::R_BACK != $args['r.type']) {
                xdbg()->err($args['r.type'], TH_DbBase::R_DIRECT != $args['r.type'], TH_DbBase::R_BACK != $args['r.type'], $args);
                throw new Exception("Invalid r.type for {$name} relation on {$this->name} ({$this->cls})");
            }

        } elseif ($rel_sz == 3) {    // with broker
            $obj_name = $args['r.path'][1];
            $binding = array(
                array(TH_DbBase::D_STORAGE, $this->name,        $args['r.key']),
                array(TH_DbBase::D_BROKER,  $args['r.path'][0]),
                array(TH_DbBase::D_STORAGE, $args['r.path'][1], $args['r.path'][2])
            );

            if (TH_DbBase::R_AUTO == $args['r.type']) {
                $args['r.type'] = TH_DbBase::R_BROKER;
            } elseif (TH_DbBase::R_BROKER != $args['r.type']) {
                throw new Exception("Invalid r.type for {$name} relation on {$this->name} ({$this->cls})");
            }
        } else {
            throw new Exception("Invalid relation path " . implode(':', $args['r.path']) . " in {$name} relation on {$this->name} ({$this->cls})");
        }

        $this->isPropExists($name, TRUE);
        $this->props[$name] = array('v.type'   => TH_DbBase::T_RELATION,
									 'v.access' => $args['r.access']
                                    );

        // TODO: make access level to be inherited!! is there any then
        // no appending, no deleteing from set if r/o mode?
        $this->props[$name]['s.value'] = new TH_DbSimpleQuery($this->tdb,
                                                               $obj_name,
                                                               $args['r.access'],
                                                               $binding,
                                                                   $this->props);

	    return $this;
	}

	/**
	 * @return TH_DbItem
	 */
	private function init_hooks() {

	    $methods = get_class_methods($this);

		$hsz = sizeof($methods);

		for ($i = 0; $i < $hsz; $i++) {
		    // hooks naming pattern: time_event__property_name

    	    $name = explode('__', $methods[$i]);

    	    // check pattern and filter methods that starts with '__'
//    	    xdbg()->warn($name, sizeof($name), sizeof($name[0]));
			if (2 != sizeof($name) || !strlen($name[0])) {    // filter unnecessary hooks
				continue;
			}

            list($event, $prop_name) = $name;

            if (!$this->isPropExists($name)) {
                continue;
            }

            $this->bind($name, array(&$this, $methods[$i]));
		}

		return $this;
	}

	/**
	 * @param string $name
	 * 			Property name to check
	 * @param bool $error
	 * 			NULL  do not trow any error, just return TRUE or FALSE
	 * 			FALSE throw error if property doesn't exists
	 * 			TRUE  throw error if property already exists
	 */
	protected function isPropExists($name, $error=NULL) {
//	    xdbg()->msg($name, debug_backtrace(0));
	    if (array_key_exists($name, $this->props)) {
	        if (TRUE === $error) {
                throw new Exception("Property $name already exists on {$this->name} ({$this->cls})");
	        }
	        return TRUE;
	    }

        if (FALSE === $error) {
            throw new Exception("Property $name does not exists on {$this->name} ({$this->cls})");
        }
	    return FALSE;
	}

	protected function isType($value, $type) {
	    switch ($type) {
	        case TH_DbBase::T_BOOL:
                return is_bool($value);
    	    break;

            case TH_DbBase::T_INT:
                return is_int($value);
            break;

            case TH_DbBase::T_FLOAT:
                return is_float($value);
            break;

            case TH_DbBase::T_STRING:
                return is_string($value);
            break;

            case TH_DbBase::T_VIRTUAL:
                // XXX: what should we do when constants become integer? what if type = T_VIRTUAL | T_<something>?
                return TRUE;    // it is alvays true because it is virtual type
            break;
            case TH_DbBase::T_RELATION:
            default:
                throw new Exception("Type is not available to check on {$this->name} ({$this->cls})");
            break;

//            default:
//                return TRUE;
//            break;

	    }
	}

    protected function setType($value, $type) {
        switch ($type) {

            case TH_DbBase::T_BOOL:
                settype($value, "bool");
            break;

            case TH_DbBase::T_INT:
                settype($value, "int");
            break;

            case TH_DbBase::T_FLOAT:
                settype($value, "float");
            break;

            case TH_DbBase::T_SERIALIZABLE:
                $value = unserialize($value);
            break;
//                TODO: relation system!

            case TH_DbBase::T_STRING:
                // XXX: may we just return value, without setting string type?
                settype($value, "string"); // used when created new (not loaded existent)
//                return $value;
            break;

            default:
                throw new Exception("Unknown type on {$this->name} ({$this->cls})");
            break;
        }

        return $value;
    }

	public function __get($name) {
	    $this->isPropExists($name, FALSE);

	    // TODO: force v.access on relation-born items
	    if (TH_DbBase::A_DENIED == $this->props[$name]['v.access']) {
	        throw new Exception("Acces denied to read {$name} property on {$this->name} ({$this->cls})");
	    }

	    // TODO: return Query object if relation
	    if (TH_DbBase::T_RELATION == $this->props[$name]['v.type']) {
    	    return $this->props[$name]['s.value'];
	    }

	    return $this->props[$name]['s.value'];
	}

	public function __set($name, $value) {
	    $this->isPropExists($name, FALSE);

	    if (TH_DbBase::A_DENIED == $this->props[$name]['v.access']) {
	        throw new Exception("Acces denied to set {$name} property on {$this->name} ({$this->cls})");
	    } elseif (TH_DbBase::A_READONLY == $this->props[$name]['v.access']) {
	        throw new Exception("Unable to set read-only property {$name} on {$this->name} ({$this->cls})");
	    } elseif (TRUE == $this->props[$name]['s.key']) {
	        throw new Exception("Unable to set manually key property {$name} on {$this->name} ({$this->cls})");
	    }

	    if (TH_DbBase::T_RELATION == $this->props[$name]['v.type']) {
	        // TODO: setting value on relational property
            throw new Exception("setting relation is not implemented yet");
//             here may be a few situations:
//             * 	In all cases it is necessary to fix ids, may be few situation:
//             * 1. R_DIRECT - change relation key in given object
//             * 2. R_BACK   - change relation key in $this object
//             * 3. R_BROKER - changes ONLY in broker table
            if (is_array($value) || is_a($value, 'TH_DbSimpleQuery')) {
//             * 1. set array         - replace all items with given into TH_DbQuery
//             * 2. set TH_DbQuery

//                $this->props[$name]['s.value']->exchangeArray($input)

            } elseif (is_a($value, 'TH_DbItem')) {
//             * 3. set single object - ???
                throw new Exception("Unable to set item directly to {$name} relation object on {$this->name} ({$this->cls})");

            } else {
                throw new Exception("Invalid {$name} relation object value type to set on {$this->name} ({$this->cls})");
            }
	    }

	    if (!$this->isType($value, $this->props[$name]['v.type'])) {
            throw new Exception("Invalid {$name} property value type to set on {$this->name} ({$this->cls})");
	    }

	    // NOTE: we may check is new value differs from initial
	    // TODO: set object status here (new, modified, etc.)
	    $old_value = $this->props[$name]['s.value'];
	    $this->props[$name]['s.value'] = $value;

	    $this->state_data &= self::S_CHANGED;

        $this->emmit("on_change__{$name}", $old_value, $value);
//        $this->emmit("on_change___object", $name, $old_value, $value);

	    return $value;
	}

	public function getState() {
	    return $this->state;
	}

	protected function get_real_props() {
	    $p = array();

	    foreach ($this->props as $k => $v) {
	        if ($v['v.type'] != TH_DbBase::T_VIRTUAL) {
                $p[$k] = $v;
	        }
	    }

	    return $p;
	}

	public function save() {
	    if (self::S_NEW && $this->state) {
        // TODO: done with saves

	        // make insert, then mark object as invalid and on first access - re-select it
	    } elseif (self::S_CHANGED && $this->state) {
	        // make update
	    } elseif (self::S_DELETE && $this->state) {
	        // make delete and remove item from collection (if any)
	    } else {
	        throw new Exception("Invalid state on {$this->name} ({$this->cls})");
	    }
	    return $this;
	}
}
?>